import { dom, Nodes } from '@knno/dom';
import { ROLLER_OPTION_LINE_HEIGHT, rollerStyle } from './roller.css';
import { DATA } from '../symbol';
import { hideScrollBar } from '../global.css';
import { drag } from '../../tools/drag';
import { play } from '../../sound/player';
import { em2px, getFontZoom } from '../../tools/utils';

export class Roller extends Nodes {
	private [DATA]: {
		pos: number;
		options: (string | number)[];
		lineHeight: number;
		calcTimer?: number;
		tmpPos: number;
		updatingScroll: boolean;
		animate: boolean;
	} = { pos: 0, options: [], lineHeight: 0, tmpPos: 0, updatingScroll: false, animate: true };

	constructor() {
		const box = dom.div(rollerStyle).attr('tabIndex', '0');
		const optionsDiv = dom.div('options', hideScrollBar).attr('tabIndex', '-1');
		box.append(dom.div('line'), optionsDiv, dom.div('mask'));
		super(box);
		optionsDiv.on('focus', () => this.focus());
		// optionsDiv.on('scroll', this.calculateValue.bind(this));
		const resizeObserver = new ResizeObserver(this.resize.bind(this));
		resizeObserver.observe(this.elems()[0]);
		this.on('click', (e) => {
			if (e.target && e.button === 0 && !moved) {
				// const l = this[DATA].lineHeight;
				let offsetTop: number;
				if (e.target === box.elems()[0]) {
					offsetTop = 0;
				} else {
					offsetTop = (e.target as HTMLElement).offsetTop;
				}
				const p = optionsDiv.scrollTop() + offsetTop + e.offsetY;
				const idx = (optionsDiv.children().elems() as HTMLElement[]).findIndex((elem) => {
					return p >= elem.offsetTop && p <= elem.offsetTop + elem.offsetHeight;
				});
				if (idx >= 0) {
					this[DATA].pos = idx;
					this.firePick();
					this.updateScroll();
				}
			}
		});
		this.on('keydown', (e) => {
			if (e.key === 'ArrowDown') {
				e.preventDefault();
				e.stopPropagation();
				this.moveDown();
			} else if (e.key === 'ArrowUp') {
				e.preventDefault();
				e.stopPropagation();
				this.moveUp();
			}
		});
		this.on('wheel', (e) => {
			e.preventDefault();
			e.stopPropagation();
			if (e.deltaY > 0) {
				this.moveDown();
			} else if (e.deltaY < 0) {
				this.moveUp();
			}
		});
		let top: number;
		let moved = false;
		let pointerDown = false;
		this.on('pointerup', (evt) => {
			evt.stopPropagation();
			evt.preventDefault();
		});
		this.on('pointerdown', (evt) => {
			if (evt.button === 0) {
				pointerDown = true;
				moved = false;
				top = optionsDiv.scrollTop();
				drag(
					this,
					evt,
					(_, distance) => {
						moved = true;
						optionsDiv.scrollTop(top - distance.y);
						this.calculateValue(false);
					},
					(_, speed) => {
						pointerDown = false;
						if (moved) {
							let speedY = speed.y;
							const doMove = () => {
								if (pointerDown) return;
								if (Math.abs(speedY) > 0.1) {
									const distance = speedY * 16;
									const top = optionsDiv.scrollTop() - distance;
									optionsDiv.scrollTop(top);
									this.calculateValue(false);
									speedY *= 0.95;
									requestAnimationFrame(doMove);
								} else {
									this.calculateValue(true);
								}
							};
							requestAnimationFrame(doMove);
						}
					}
				);
			}
		});
		this.on(
			'touchmove',
			(evt) => {
				evt.preventDefault();
				evt.stopPropagation();
			},
			true
		);
	}

	private firePick() {
		// if (this[DATA].pos === 0) debugger;
		// console.log('roller change: ' + this[DATA].options[this[DATA].pos]);
		this.emit(new Event('change'));
	}

	moveDown() {
		if (this[DATA].pos < this[DATA].options.length - 1) {
			this[DATA].pos++;
			this.firePick();
			this.updateScroll();
		}
	}

	moveUp() {
		if (this[DATA].pos > 0) {
			this[DATA].pos--;
			this.firePick();
			this.updateScroll();
		}
	}

	private resize() {
		const optDiv = this.query('div.options');
		const lineHeight = em2px(optDiv.elems()[0] as HTMLElement, ROLLER_OPTION_LINE_HEIGHT) / getFontZoom();
		const margin = (this.innerHeight() - lineHeight) / 2 + 'px';
		optDiv.css('paddingTop', margin);
		optDiv.css('paddingBottom', margin);
		const line = this.query('div.line');
		const top = (this.innerHeight() + lineHeight) / 2 - lineHeight + 'px';
		line.css('top', top);
		this[DATA].lineHeight = lineHeight;
		this.animate(false);
		this.updateScroll();
		this.animate(true);
	}

	private updateScroll() {
		if (this[DATA].updatingScroll) return;
		this[DATA].updatingScroll = true;
		const optDiv = this.query('div.options');
		const l = this[DATA].lineHeight;
		if (!this[DATA].animate) {
			const targetPos = this[DATA].pos * l;
			optDiv.elems()[0].scrollTo({ top: targetPos, behavior: 'instant' });
			this[DATA].updatingScroll = false;
			return;
		}
		const doUpdate = () => {
			const targetPos = this[DATA].pos * l;
			const current = optDiv.scrollTop();
			const distance = targetPos - current;
			if (Math.abs(distance) > 1) {
				const direction = distance > 0 ? 1 : -1;
				// y = a * (x ** 2)  ==>  x = sqrt(y / a)
				const move = Math.sqrt(Math.abs(distance) / 0.9) * direction;
				if (Math.abs(distance) > 1) {
					optDiv.elems()[0].scrollTo({ top: current + move, behavior: 'instant' });
				} else {
					optDiv.elems()[0].scrollTo({ top: targetPos, behavior: 'instant' });
				}
				this.calculateValue(false, true);
				requestAnimationFrame(doUpdate);
			} else {
				this[DATA].updatingScroll = false;
			}
		};
		requestAnimationFrame(doUpdate);
	}

	private calculateValue(scroll: boolean, playOnly = false) {
		const optDiv = this.query('div.options');
		if (optDiv.height() <= 0) return;
		const pos = Math.round(optDiv.scrollTop() / this[DATA].lineHeight);
		if (this[DATA].tmpPos !== pos) {
			this[DATA].tmpPos = pos;
			play('da');
		}
		if (!playOnly) {
			if (this[DATA].pos !== pos) {
				this[DATA].pos = pos;
				this.firePick();
			}
		}
		scroll && this.updateScroll();
	}

	options(): (string | number)[];
	options(options: (string | number)[]): this;
	options(options?: (string | number)[]): this | (string | number)[] {
		if (options === undefined) {
			return this[DATA].options;
		}
		const oldValue = this.value();
		this[DATA].options = options;
		const optDiv = this.query('div.options');
		optDiv.html('').append(
			...options.map((s) => {
				return dom.div().text(s + '');
			})
		);
		if (oldValue) {
			this.value(oldValue);
		} else {
			this[DATA].pos = 0;
		}
		this.animate(false);
		this.updateScroll();
		this.animate(true);
		return this;
	}

	value(): string | number | null;
	value(value: string | number): this;
	value(value?: string | number | undefined): this | string | number | null {
		if (value === undefined) {
			return this[DATA].options[this[DATA].pos] ?? null;
		}
		const pos = this[DATA].options.indexOf(value);
		if (pos < 0) {
			if (this[DATA].pos >= this[DATA].options.length) {
				this[DATA].pos = this[DATA].options.length - 1;
			}
		} else {
			this[DATA].pos = pos;
		}
		this.animate(false);
		this.updateScroll();
		this.animate(true);
		return this;
	}

	selected(): number;
	selected(value: number): this;
	selected(value?: number): this | number {
		if (value === undefined) {
			return this[DATA].options.length === 0 ? -1 : this[DATA].pos;
		}
		this[DATA].pos = value;
		if (this[DATA].pos >= this[DATA].options.length) {
			this[DATA].pos = this[DATA].options.length - 1;
		} else if (this[DATA].pos < 0) {
			this[DATA].pos = 0;
		}
		this.updateScroll();
		return this;
	}

	animate(value: boolean): this {
		this[DATA].animate = value;
		return this;
	}
}

export function roller(...className: string[]): Roller {
	return new Roller().addClass(...className);
}
